Function Scope Block Scope

함수 기반 스코프

자바스크립트는 함수 기반 스코프를 사용한다.

즉 각각의 선언된 함수는 저 마다의 버블을 생성하지만 다른 어떤 자료 구조도 자체적인 스코프를 생성하지 못한다.

이는 정확한 표현은 아니다.

function foo(a) {
var b = 2;
function bar() {}
var c = 3;
}

food의 스코프 버블은 확인자 a,b,c, bar를 포함한다.

선언문이 스코프 어디에 있는지 중요하지 않는다.

스코프 안에 있는 모든 변수와 함수는 그 스코프 버블에 속한다.

a,b,c,bar 모두 foo의 스코프 버블에 속하므로 foo 바깥에서는 이들에게 접근할 수 없다.

다음 코드는 호출된 확인자가 글로벌 스코프에는 없기 떄문에 ReferenceError를 발생시킨다.

bar();
console.log(a, b, c);

a,b,c, foo, bar는 foo 안에서 접근할 수 있고 bar안에서도 새도잉 확인자가 없다면 사용할 수 있다.

함수 스코프는 모든 변수가 함수에 속하고 함수 전체에 걸쳐 사용되며 재사용된다는 개념을 확고하게 한다.

일반 스코프에 숨기

함수에 대한 전통적인 개념은 다음과 같다.

  1. 함수를 선언하고 그 안에 코드를 넣는다.

  2. 작성한 코드에서 임의 부분을 함수 선언문으로 감싼다. 해당 코드를 숨기는 효과를 나타낸다.

이렇게 하면 해당 코드 주위에 새로운 스코프 버블이 생긴다.

즉 감싸진 코드 안에 있는 모든 변수 또는 함수 선언문은 이전 코드에 포함됐던 스코프가 아니라 새로이 코드를 감싼 함수의 스코프에 묶인다.

달리 말하면 함수의 스코프로 둘러싸서 변수와 함수를 숨길 수 있다.

스코프를 이용해 숨기는 방식을 사용하는 이유는 여러 가지가 있는디 소프트웨어 디자인 원칙인 최소 권한의 원칙과 관련있다.

이 원칙은 모듈/객체의 API와 같은 소프트웨어를 설계할 때 필요한 것만 남기고 나머지는 숨겨야한다는 것이다.

모든 변수와 함수가 글로벌 스코프에 존재한다면 어느 중첩된 하위 스코프에서도 이들에 접근할 수 있다.

이는 최소 권한의 원칙을 어기는 것이고 코드를 적절하게 사용했을때 접근할 필요가 없어서 비공개로 남겨둬야 할 많은 변수나 함수를 노출시키게 된다.

function doSomething(a) {
b = a + doSomethingElse(a * 2);
console.log(b * 3);
}
function doSomethingElse(a) {
return a - 1;
}
var b;
doSomething(2);

변수 b와 함수 doSomethingElse는 doSomething이 어떤 작업을 하는지 보여주는 비공개 부분이라고 할 수 있다.

이 두 확인자에 접근할수 있도록 내버려 두는 것은 불필요할 뿐 아니라 위험할 수 있다.

그래서 앞의 비공개 부분은 doSomething 스코프 내부에 숨겨야 한다.

function doSomething(a) {
function doSomsethingElse(a) {
return a - 1;
}
var b;
b = a + doSomethingElse(a * 2);
console.log(b * 3);
}
doSomething(2);

충돌 회피

변수와 함수를 스코프 안에 숨기는 것의 또 다른 장점은 같은 이름을 가졌지만 다른 용도인 두 확인자가 충돌하는 것을 피할 수 있다.

이런 충돌은 예상하지 못한 변수값의 겹쳐 쓰기를 초래한다.

function foo() {
function bar() {
i = 3;
console.log(a + i);
}
for (var i = 0; i < 10; i++) {
bar(i * 2);
}
}
foo();

bar 안에 있는 i가 반복문을 위해 선언된 변수 i의 값을 변경한다.

bar 내부의 대입문은 어떤 확인자 이름을 고르든 지역 변수로 선언해서 사용해야 한다.

그러나 소프트웨어 설계를 하다 보면 자연스럽게 같은 확인자 이름을 사용하게 되므로 스코프를 이용해서 내부에 선언문을 숨기는 것이 가장 좋은, 유일한 선택지다.

글로벌 네임 스페이스

글로벌 스코프에서는 변수 충돌이 쉽게 일어난다. 내부/비공개 함수와 변수가 적절하게 숨겨져 있지 않는 여러 라이브러리를 한 프로그램에 블러오면 라이브러리 끼리 서로 쉽게 충돌 할수 있다.

라이브러리는 일반적으로 글로벌 스코프에 하나의 고유 이름을 가지는 객체 선언문을 생성한다.

이후 객체는 해당 라이브러리의 네임스페이스로 이용된다.

네임스페이스를 통해 최상위 스코프의 확인자가아니라 속성 형태로 라이브러리의 모든 기능이 노출된다.

모듈 관리

좀 더 현대적인 충돌 방지 옵션으로는 다양한 의존성 관리자를 이용한 모듈 접근법이 있다.

이 도구를 사용하면 어떤 라이브러리도 확인자를 글로벌 스코프에 추가할 필요없이 특정 스코프로부터 의존성 관리자를 이용한 다양한 명시적인 방법으로 확인자를 가져와 사용할 수 있다.

의존성 관리자는 스코프 규칙을 적용해 모든 확인자가 공유 스코프에 노출되는 것을 방지하고 우발적인 스코프 충돌을 예방하기 위해 비공개 스코프에 확인자를 보관한다.

스코프 역할을 하는 함수

var a = 2;
function foo() {
var a = 3;
console.log(a);
}
foo();
console.log(a);

코드를 함수로 감싸 내부에 변수나 함수 선언문을 바깥 스코프로부터 함수의 스코프 안에 숨겼다.

하지만 이 방식에는 몇 가지 문제점이 있다.

첫째, foo라는 함수를 선언해야한다.

foo라는 확인자 이름으로 글로벌 스코프를 오염시킨다.

또한 그 함수를 직접 이름으로 호출해야 실제 감싼 코드를 실행할 수 있다.

함수를 이름없이 선언하고 자동으로 실행된다면 더 이상적일 것이다.

var a = 2;
(function() {
var a = 3;
console.log(a);
})();
console.log(a);

이 코드는 선언문이 아니라 함수 표현식으로 취급된다.

함수 선언문과 표현식의 중요한 차이는 함수 이름이 어디의 확인자로 묶이느냐와 관련이 있다.

첫 번째 코드에서 함수 foo는 함수를 둘러싼 스코프에 묶이고 foo라는 이름을 통해 직접 호출을 했다.

두 번째 코드에서 함수 이름 foo는 함수를 둘러싼 스코프에 묶이는 대신 함수 자신 내부 스코프에 묶였다.

(function foo() {})라는 표현식에서 확인자 foo는 자신만의 스코프에서만 찾을 수 있고 바깥 스코프에서는 발견되지 않는다.

익명 vs 기명

setTimeout(function() {
console.log("i waited 1 second");
}, 1000);

이런 방식을 익명 함수 표현식이라고 하는데 이는 확인자 이름이 없기 때문이다.

함수 표현식은 이름 없이 사용할 수 있지만 함수 선언문에는 이름이 빠져서는 안된다.

하지만 몇가지 단점이 있다.

  1. 익명 함수는 스택 추적 시 표시할 이름이 없어서 디버깅이 더 어려울 수 있다.

  2. 이름 없이 함수 스스로 재귀 호출을 하려면 폐기 예정인 arguments.callee 참조가 필요하다.

  3. 이름은 보통 쉽게 이해하고 읽을 수 있는 코드 작성에 도움이 된다.

인라인 함수 표현식은 매우 효과적이고 유용하다. 함수 표현식에 이름을 사용하면 특별한 부작용 없이 상당히 효과적으로 앞의 단점을 해결할 수 있다.

setTimeout(function timeoutHandler() {
console.log("i waited 1 second");
}, 1000);

함수 표현식 즉시 호출하기

var a = 2;
(function() {
var a = 3;
console.log(a);
})();
console.log(a);

()로 함수를 감싸면 함수를 표현식으로 바꾸는데 마지막에 또 다른()를 붙이면 함수를 실행할수 있다.

함수를 둘러 싼 첫 번째 ()는 함수를 표현식으로 바꾸고 두번쨰 ()는 함수를 실행시킨다.

이것을 즉시 호출 함수 표현식이라고 IIFE라고 부른다.

IIFE는 익명 함수 표현식으로 가장 흔하게 사용된다. 기명 IIFE를 사용하는 것은 좋은 습관이다.

전통적인 IIFE 형태를 변형하여 (function(){}())로 사용하기도 한다.

첫 번쨰 형태에서 함수 표현식은 ()안에 싸여있고 호출에 사용되는 ()가 밖에 바로 붙어 있다.

둘쨰 형태에서 호출에 사용되는 ()는 둘러싼 ()안으로 옮겨졌다.

var a = 2;
(function(global) {
var a = 3;
console.log(a);
console.log(global.a);
})(window);
console.log(a);

예제에서 window 객체 참조를 global이라 이름을 붙인 인자에 넘겨서 글로벌 참조와 비 글로벌 참조 사이에 명확한 차이를 만들었다.

해당 스코프에 무엇이든 넘길 수 있고 인자 이름도 마음대로 지을 수 있다.

IIFE의 변형된 형태가있다. 여기서 실행할 함수는 호출문과 넘겨진 인자 뒤쪽에 온다.

이 패턴은 UMD 프로젝트에서 사용한다.

var a = 2;
(function IIFE(def) {
def(window);
})(function def(global) {
var a = 3;
console.log(a);
console.log(global.a);
});

스코프 역할을 하는 블록

for (var i = 0; i < 10; i++) {
console.log(i);
}

변수 i를 반복문의 시작부에 선언하는 이유는 보통 i를 오직 for 문에 사용하려 하기 때문이다.

그리고 변수 i가 실제로 둘러싼 스코프에 포함된다는 사실을 무시한다.

블록 스코프의 목적은 변수를 최대한 사용처 가까이에 최대한 작은 유효 범위를 갖도록 선언하는 것이다.

var foo = true;
if (foo) {
var bar = foo * 2;
bar = something(bar);
console.log(bar);
}

변수 bar는 오직 if문 안에서만 사용하므로 bar를 if 블록 안에 선언하는 것이 타당하다.

그러나 var를 사용할때 변수를 어디에 선언하는지는 중요하지 않다. 선언된 변수는 둘러쌓인 스코프에 속하기 때문이다.

블록 스코프는 최소 권한 노출의 원칙을 확장하여 정보를 함수 안에 숨기고 나아가 정보를 코르 블록안에 숨기기 위한 도구다.

for (var i = 0; i < 10; i++) {
console.log(i);
}

오직 for 문에서만 사용할 변수 i로 함수 스코프 전체가 오염이 된다.

블록 스코프를 사용한다면 변수 i는 오직 for 문에서만 사용할 수 있고 이 외 함수 어느 곳에서 접근하더라도 오류가 발생할 것이다.

with

with 문 인자로 들어간 객체로 부터 생성된 스코프는 with 선언문이 끝날 때까지만 존재한다.

try/catch

catch 부분에서 선언된 변수는 catch 블록 스코프에 속한다.

try {
// ...
} catch (err) {
console.log(err);
}
console.log(err);

변수 err는 오직 catch 문 안에만 존재하므로 다른 돗에서 참조하면 오류가 발생한다.

let

ES6에서 새로운 키워드 let을 추가했다.

let은 var와 같이 변수를 선언하는 다른 방식이다.

키워드 let은 선언된 변수를 둘러싼 아무 블록의 스코프에 붙인다.

명시적이지 않지만 let은 선언한 변수를 위해 해당 블록 스코프를 이용한다고 말할 수 있다.

var foo = true;
if (foo) {
let bar = foo * 2;
bar = something(bar);
console.log(bar);
}
console.log(bar); // ReferenceError

그저 {}을 문법에 맞게 추가만 해도 let을 통해 선언된 변수를 묶을 수 있는 임의의 블록을 생성할 수 있다.

호이스팅은 선언문이 어디에서 선언됐든 속하는 스코프 전체에서 존재하는 것처럼 취급되는 작용을 말한다.

그러나 let을 사용하는 선언문은 속하는 스코프에서 호이스팅 효과를받지 않는다.

따라서 let으로 선언된 변수는 실제 선언문 전에는 명백하게 존재하지 않는다.

가비지 콜렉션

블록 스코프가 유용한 또 다른 이유는 메모리를 회수하기 위한 클로저 그리고 가비지 콜렉션과 관련이 있다.

function process(data) {}
var someReallyBigData = {};
process(someReallyBidData);
var btn = document.getElementById("my_button");
btn.addEventListener("click", function(evt) {
console.log("button clicked");
});

클릭을 처리하는 click 함수는 someRellyBidData 변수가 전혀 필요 없다.

따라서 이론적으로 process가 실행된 다음에는 많은 메모리를 먹는 자료 구조인 someReallyBigData는 수거 할 수도 있다.

하지만 click 함수가 해당 스코프 전체의 클로저를 가지고 있기 떄문에 남겨 둘것이다.

블록 스코프는 엔진에게 someReallyBigData가 더는 필요 없다는 사실을 더 명료하게 알려서 이 문제를 해결할 수 있다.

function process(data) {}
{
let someReallyBigData = {};
process(someReallyBidData);
}
var btn = document.getElementById("my_button");
btn.addEventListener("click", function click(evt) {
console.log("button clicked");
});

명시적으로 블록을 선언하여 변수의 영역을 한정하는 것은 효과적인 코딩 방식이다.

let 반복문

for (let i = 0; i < 10; i++) {
console.log(i);
}
console.log(i); // ReferenceError

let은 i를 for 문에 묶을 뿐만 아니라 반복문이 돌 때마다 변수를 다시 묶어서 이전 반복의 결과값이 제대로 들어간다.

let 선언문은 둘러싼 함수 스코프가 아니라 가장 가까운 임의의 블록에 변수를 붙인다.

var foo = true,
baz = 10;
if (foo) {
var bar = 3;
if (baz > bar) {
console.log(baz);
}
}
// 다음과 같이 리팩토링이 가능하다.
var foo = true,
baz = 10;
if (foo) {
var bar = 3;
}
if (baz > bar) {
console.log(baz);
}
// 하지만 다음 과 같은 상황은 주의해야 한다.
var foo = true,
baz = 10;
if (foo) {
let bar = 3;
if (baz > bar) {
console.log(baz);
}
}

const

ES6에서는 키워드 let과 함께 const도 추가됐다. 키워드 const 역시 블록 스코프를 생성하지만, 선언된 값은 고정된다.

선언된 후 const의 값을 변경하려고 하면 오류가 발생한다.

var foo = true;
if (foo) {
var a = 2;
const b = 3;
a = 3;
b = 4; // error
}
console.log(a);
console.log(b); // ReferenceError

정리하기

자바스크립트에서 함수는 스코프를 이루는 가장 흔한 단위이다.

다른 함수 안에서 선언된 변수와 함수는 다른 스코프로 부터 숨겨진 것이다.

이는 좋은 소프트웨어를 위해 적용해야할 디자인 원칙이다.

블록 스코프는 함수만이 아니라 임의의 코드 블록에 변수와 함수가 속하는 개념이다.

try/catch 구조의 catch 부분은 블록 스코프를 가진다.

ES6에서는 let이 추가되어 임의의 코드 블록 안에 변수를 선언할 수 있게 되었다.

블록 스코프는 var 함수 스코프를 완전히 대체할 수 없다.